/*
 * Copyright (C) 2010 Fred Barrie This program is free software: you can
 * redistribute it and/or modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version. This program is distributed
 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
 * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU General Public License for more details. You should have received
 * a copy of the GNU General Public License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 */
package org.aergia.vinny.engine;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.TreeMap;
import java.util.logging.Logger;

import org.aergia.vinny.core.ApiResult;
import org.aergia.vinny.core.enums.DayOfWeek;
import org.aergia.vinny.core.enums.TimeFrame;
import org.aergia.vinny.database.Database;
import org.aergia.vinny.database.pojo.Card;
import org.aergia.vinny.database.pojo.Location;
import org.aergia.vinny.database.pojo.OperatingHours;
import org.aergia.vinny.database.pojo.Organization;
import org.aergia.vinny.database.pojo.Scan;
import org.aergia.vinny.database.pojo.Service;
import org.aergia.vinny.database.pojo.ServiceType;
import org.aergia.vinny.database.pojo.Skill;
import org.aergia.vinny.database.pojo.SkillRate;
import org.aergia.vinny.database.pojo.VinnyOption;
import org.aergia.vinny.database.pojo.VinnyRole;
import org.aergia.vinny.database.pojo.VinnyUser;
import org.aergia.vinny.database.pojo.Volunteer;
import org.aergia.vinny.database.pojo.VolunteerGroup;
import org.aergia.vinny.database.pojo.Workstation;
import org.aergia.vinny.engine.servicetime.ServiceTimeCalculator;
import org.aergia.vinny.engine.servicetime.ServiceTimeCalculatorFactory;
import org.aergia.vinny.util.CollectionsUtil;
import org.aergia.vinny.util.DateUtil;
import org.aergia.vinny.util.Md5Util;
import org.aergia.vinny.util.ValidateUtil;

/**
 * @author fred
 */
public final class EngineImpl extends Object implements Engine {

	private static final String GENERIC_VOLUNTEER_NAME = "Unknown";
	/**
	 * Length of the Vinny Card Number
	 */
	private static final int VINNY_CARD_NUMBER_LENGTH = 7;
	private VinnyUser admin;
	private final Database database;
	private Location location;
	private final Logger logger;
	private String productName;
	private final ServiceTimeCalculator serviceTimeCalculator;
	private Workstation workstation;

	public EngineImpl(final Database database) {
		this.logger = Logger.getLogger(EngineImpl.class.getCanonicalName());
		this.database = database;
		this.admin = this.getAdminUser();
		this.serviceTimeCalculator = ServiceTimeCalculatorFactory.newInstance(this.database);
	}

	@Override
	public ApiResult<Card> addCard(final String cardNumber, final Volunteer volunteer, final Calendar startDate, final VinnyUser user) {
		ValidateUtil.assertNotNull(cardNumber, "Card Number must be specified.");
		ValidateUtil.assertNotNull(volunteer, "Volunteer must be specified.");
		ValidateUtil.assertNotNull(user, "The user must be specified.");

		this.logger.fine("Added card : " + cardNumber + " " + volunteer.getId() + " " + user.getId());

		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			final String warning = "User " + user.getUserName() + " does not have permission to modify a volunteer.";
			this.logger.warning(warning);
			return new ApiResult<Card>(true, warning);
		}

		final ApiResult<Card> cardResult = this.database.getCard(cardNumber);
		if (cardResult.getResult() != null) {
			final Card card = cardResult.getResult();
			if (!card.getVolunteer().equals(volunteer)) {
				final String warning = "Card " + cardNumber + " is already assigned to " + card.getVolunteer().getFirstName() + " "
						+ card.getVolunteer().getLastName() + ".";
				this.logger.warning(warning);
				return new ApiResult<Card>(true, warning);
			}

			if (!card.isActive()) {
				card.setActive(true);
				this.database.saveCard(card);
			}
			return new ApiResult<Card>(card);
		}

		final Card card = new Card(cardNumber, volunteer, startDate);
		volunteer.setCard(card);
		this.database.addCard(card);
		this.database.saveVolunteer(volunteer);
		this.logger.fine("Added card : " + card);
		return new ApiResult<Card>(card);
	}

	@Override
	public ApiResult<Volunteer> addGenericVolunteer(final String cardNumber, final Calendar startDate) {

		ValidateUtil.assertNotNull(cardNumber, "Card number must be specified.");
		ValidateUtil.assertNotNull(startDate, "Start date must be specified.");

		ApiResult<Volunteer> apiResult = null;

		if (!this.isVinnyCard(cardNumber)) {
			return new ApiResult<Volunteer>(true, "Invalid scan. Please re-scan your \"" + this.getProductName() + "\" card.");
		}

		final ApiResult<Skill> result = this.database.getSkill(Skill.GENERAL_SKILL);
		if (result.isError()) {
			return new ApiResult<Volunteer>(true, result.getText());
		}
		final Skill skill = result.getResult();

		final Volunteer volunteer = new Volunteer();
		volunteer.setFirstName(EngineImpl.GENERIC_VOLUNTEER_NAME);
		volunteer.setLastName(EngineImpl.GENERIC_VOLUNTEER_NAME);
		volunteer.setSkills(Collections.singleton(skill));

		final Card card = new Card();
		card.setNumber(cardNumber);
		card.setStartDate(startDate);
		card.setEndDate(DateUtil.FAR_IN_FUTURE);
		card.setVolunteer(volunteer);
		card.setComment("Generic card added by system.");

		apiResult = this.database.saveVolunteer(volunteer);
		if (apiResult.isError()) {
			return apiResult;
		}

		final ApiResult<Card> cardResult = this.database.saveCard(card);
		if (cardResult.isError()) {
			return new ApiResult<Volunteer>(true, cardResult.getText());
		}

		return new ApiResult<Volunteer>(volunteer);
	}

	@Override
	public ApiResult<VolunteerGroup> addGroup(final String groupName, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<VolunteerGroup>(true, "User " + user.getUserName() + " does not have permission to add a volunteer group.");
		}
		final VolunteerGroup group = new VolunteerGroup(groupName, this.location);
		return this.database.addGroup(group);
	}

	@Override
	public ApiResult<Service> addService(final Volunteer volunteer, final Calendar date, final int duration, final TimeFrame timeFrame,
			final ServiceType serviceType, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_SERVICE)) {
			return new ApiResult<Service>(true, "User " + user.getUserName() + " does not have permission to add a service.");
		}

		final Skill skill = CollectionsUtil.getFirst(volunteer.getSkills());
		final Service service = new Service(volunteer, serviceType, skill, date, duration, timeFrame, user);
		return this.database.addService(service);
	}

	@Override
	public ApiResult<ServiceType> addServiceType(final String name, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<ServiceType>(true, "User " + user.getUserName() + " does not have permission to add a service type.");
		}

		final ServiceType serviceType = new ServiceType(name, this.location);
		return this.database.addServiceType(serviceType);
	}

	@Override
	public ApiResult<Skill> addSkill(final Skill skill, final SkillRate skillRate, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<Skill>(true, "User " + user.getUserName() + " does not have permission to add a new skill.");
		}

		final ApiResult<Skill> skillResult = this.database.addSkill(skill);
		if (skillResult.isError()) {
			return skillResult;
		}

		final ApiResult<SkillRate> skillRateResult = this.database.addSkillRate(skillRate);
		if (skillRateResult.isError()) {
			return new ApiResult<Skill>(skillRateResult);
		}

		return new ApiResult<Skill>(skillResult.getResult());
	}

	@Override
	public ApiResult<SkillRate> addSkillRate(final Skill skill, final SkillRate skillRate, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<SkillRate>(true, "User " + user.getUserName() + " does not have permission to modify a new skill.");
		}

		final ApiResult<SkillRate> skillRateResult = this.database.getSkillRate(skill);

		if (skillRateResult.isError()) {
			return skillRateResult;
		}

		final SkillRate currentSkillRate = skillRateResult.getResult();
		currentSkillRate.setEndDate(skillRate.getStartDate());
		final ApiResult<SkillRate> skillRateResult2 = this.database.saveSkillRate(currentSkillRate);

		if (skillRateResult2.isError()) {
			return skillRateResult2;
		}

		return this.database.addSkillRate(skillRate);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.aergia.vinny.engine.Engine#addSkillToVolunteer(org.aergia.vinny.database
	 * .pojo.Volunteer, org.aergia.vinny.database.pojo.Skill,
	 * org.aergia.vinny.database.pojo.VinnyUser)
	 */
	@Override
	public ApiResult<Volunteer> addSkillToVolunteer(final Volunteer volunteer, final Skill skill, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to modify a volunteer.");
		}

		if (volunteer.getSkills().contains(skill)) {
			return new ApiResult<Volunteer>(volunteer);
		}
		volunteer.addSkill(skill);
		return this.database.saveVolunteer(volunteer);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.aergia.vinny.engine.Engine#addUser(org.aergia.vinny.database.pojo
	 * .VinnyUser, org.aergia.vinny.database.pojo.VinnyUser)
	 */
	@Override
	public ApiResult<VinnyUser> addUser(final VinnyUser newUser, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_USER)) {
			return new ApiResult<VinnyUser>(true, "User " + user.getUserName() + " does not have permission to add a new user.");
		}
		return this.database.addUser(newUser);
	}

	@Override
	public ApiResult<Volunteer> addVolunteer(final String firstName, final String lastName, final VinnyUser user) {
		ValidateUtil.assertNotNull(firstName, "First name must be specified.");
		ValidateUtil.assertNotNull(lastName, "Last name must be specified.");
		ValidateUtil.assertNotNull(user, "The user must be specified.");

		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to add a new volunteer.");
		}
		final ApiResult<Skill> result = this.database.getSkill(Skill.GENERAL_SKILL);
		if (result.isError()) {
			return new ApiResult<Volunteer>(true, result.getText());
		}
		final Skill skill = result.getResult();
		final Volunteer volunteer = new Volunteer(firstName, lastName, skill);

		return this.database.addVolunteer(volunteer);
	}

	@Override
	public ApiResult<Workstation> addWorkstation(final String name, final VinnyUser user) {
		ValidateUtil.assertNotNull(name, "Workstation name must be specified.");

		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<Workstation>(true, "User " + user.getUserName() + " does not have permission to add a workstatio.");
		}

		final Workstation newWorkstation = new Workstation();
		newWorkstation.setActive(true);
		newWorkstation.setLocation(this.location);
		newWorkstation.setName(name);
		return this.database.saveWorkstation(newWorkstation);
	}

	@Override
	public ApiResult<?> authorizeAllServices() {
		final boolean autoAuthorize = Boolean.parseBoolean(this.getOption(VinnyOption.AUTO_AUTHORIZE, "false"));
		if (!autoAuthorize) {
			return new ApiResult<Object>();
		}

		final Collection<Service> services = this.database.getUnapprovedServices();
		return this.authorizeServices(services, this.admin);
	}

	@Override
	public ApiResult<?> authorizeServices(final Collection<? extends Service> services, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_SERVICE)) {
			return new ApiResult<Object>(true, "User " + user.getUserName() + " does not have permission to modify service records.");
		}
		for (final Service service : services) {
			if (this.isAutoAuthoriable(service)) {
				service.setApproved(true);
				service.setApprovedBy(user);
				this.database.saveService(service);
			}
		}
		return new ApiResult<Object>();
	}

	@Override
	public void calculateService() {
		this.serviceTimeCalculator.calculateService();
	}

	@Override
	public ApiResult<OperatingHours> changeOperatingHours(final OperatingHours newHours, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<OperatingHours>(true, "User " + user.getUserName() + " does not have permission to modify the operating hours.");
		}
		return this.database.saveOperatingHours(newHours);
	}

	@Override
	public ApiResult<?> denyServices(final Collection<Service> services, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_SERVICE)) {
			return new ApiResult<Object>(true, "User " + user.getUserName() + " does not have permission to modify service records.");
		}
		for (final Service service : services) {
			service.setApproved(false);
			service.setApprovedBy(user);
			this.database.saveService(service);
		}
		return new ApiResult<Object>();
	}

	@Override
	public ApiResult<Card> getCard(final String cardNumber) {
		return this.database.getCard(cardNumber);
	}

	@Override
	public Collection<? extends Card> getCards() {
		return this.database.getCards();
	}

	@Override
	public Database getDatabase() {
		return this.database;
	}

	@Override
	public Location getFirstLocation() {
		final Collection<? extends Location> locations = this.database.getLocations();
		return CollectionsUtil.getFirst(locations);
	}

	@Override
	public ApiResult<VolunteerGroup> getGroup(final String groupName) {
		return this.database.getGroup(groupName, this.location);
	}

	@Override
	public Collection<? extends VolunteerGroup> getGroups() {
		return this.database.getGroups();
	}

	@Override
	public OperatingHours getHours(final DayOfWeek day) {
		final ApiResult<OperatingHours> hoursResult = this.database.getOperatingHours(day);
		if (hoursResult.isError()) {
			return new OperatingHours(day, 0, 0);
		}
		return hoursResult.getResult();
	}

	@Override
	public ApiResult<Location> getLocation(final int id) {
		return this.database.getLocation(id);
	}

	@Override
	public String getOption(final String name, final String defaultValue) {
		final ApiResult<VinnyOption> result = this.database.getOption(name);
		if (result.isError()) {
			return defaultValue;
		}
		return result.getResult().getValue();
	}

	@Override
	public Collection<? extends VinnyOption> getOptions() {
		return this.database.getOptions();
	}

	@Override
	public ApiResult<VinnyRole> getRole(final String permission) {
		return this.database.getRole(permission);
	}

	@Override
	public Collection<? extends VinnyRole> getRoles() {
		return this.database.getRoles();
	}

	@Override
	public Collection<? extends Scan> getScans() {
		return this.database.getScans();
	}

	@Override
	public Collection<? extends Service> getServices() {
		this.calculateService();
		return this.database.getServices();
	}

	@Override
	public ApiResult<ServiceType> getServiceType(final String name) {
		return this.database.getServiceType(name, this.location);
	}

	@Override
	public Collection<? extends ServiceType> getServiceTypes() {
		return this.database.getServiceTypes();
	}

	@Override
	public ApiResult<Skill> getSkill(final String skillName) {
		return this.database.getSkill(skillName);
	}

	@Override
	public Collection<? extends VinnyUser> getUsers() {
		return this.database.getUsers();
	}

	@Override
	public Collection<VinnyUser> getUsers(final String inRole) {
		return this.database.getUsers(inRole);
	}

	@Override
	public ApiResult<Volunteer> getVolunteer(final String cardNumber) {
		final ApiResult<Card> cardResult = this.database.getCard(cardNumber);
		if (!cardResult.isError()) {
			return this.database.getVolunteer(cardResult.getResult().getId());
		}
		return new ApiResult<Volunteer>(true, cardResult.getText());
	}

	@Override
	public Collection<? extends Volunteer> getVolunteers() {
		return this.database.getVolunteers();
	}

	@Override
	public ApiResult<Workstation> getWorkstation(final int id) {
		return this.database.getWorkstation(id);
	}

	@Override
	public Collection<? extends Workstation> getWorkstations() {
		return this.database.getWorkstations(this.location);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.aergia.vinny.engine.Engine#installLocation(org.aergia.vinny.database
	 * .pojo.Organization, org.aergia.vinny.database.pojo.Location,
	 * org.aergia.vinny.database.pojo.OperatingHours[], int)
	 */
	@Override
	public ApiResult<Location> installLocation(final Organization organization, final Location installLocation, final Collection<OperatingHours> hours,
			final int rate, final Calendar date) {
		this.location = installLocation;

		final ApiResult<Organization> orgResult = this.database.addOrganization(organization);
		if (orgResult.isError()) {
			this.logger.severe(orgResult.getText());
			return new ApiResult<Location>(orgResult);
		}
		installLocation.setOrganization(organization);
		final ApiResult<Location> locResult = this.database.addLocation(installLocation);
		if (locResult.isError()) {
			this.logger.severe(locResult.getText());
			return locResult;
		}
		for (final OperatingHours operatingHours : hours) {
			operatingHours.setLocation(installLocation);
			final ApiResult<OperatingHours> hoursResult = this.database.addOperatingHours(operatingHours);
			if (hoursResult.isError()) {
				this.logger.severe(hoursResult.getText());
				return new ApiResult<Location>(hoursResult);
			}
		}
		final Collection<VinnyRole> vinnyRoles = new ArrayList<VinnyRole>();
		final String[] roles = { VinnyRole.MODIFY_CONFIGURATION, VinnyRole.MODIFY_SERVICE, VinnyRole.MODIFY_USER, VinnyRole.MODIFY_VOLUNTEER };
		for (final String role : roles) {
			final VinnyRole vinnyRole = new VinnyRole(role);
			final ApiResult<VinnyRole> roleResult = this.database.addRole(vinnyRole);
			if (roleResult.isError()) {
				this.logger.severe(roleResult.getText());
				return new ApiResult<Location>(roleResult);
			}
			vinnyRoles.add(roleResult.getResult());
		}

		this.admin = new VinnyUser("admin");
		this.admin.setRoles(vinnyRoles);
		this.admin.setPassword(Md5Util.encode("password"));
		this.admin.setLocation(installLocation);
		final ApiResult<VinnyUser> userResult = this.database.addUser(this.admin);
		if (userResult.isError()) {
			this.logger.severe(userResult.getText());
			return new ApiResult<Location>(userResult);
		}

		final Skill skill = new Skill(Skill.GENERAL_SKILL);
		skill.setLocation(installLocation);
		final ApiResult<Skill> skillResult = this.database.addSkill(skill);
		if (skillResult.isError()) {
			this.logger.severe(skillResult.getText());
			return new ApiResult<Location>(skillResult);
		}
		final SkillRate skillRate = new SkillRate(skill, rate, date);
		final ApiResult<SkillRate> skillRateResult = this.database.addSkillRate(skillRate);
		if (skillRateResult.isError()) {
			this.logger.severe(skillRateResult.getText());
			return new ApiResult<Location>(skillRateResult);
		}

		final VolunteerGroup group = new VolunteerGroup(VolunteerGroup.GENERAL_GROUP, installLocation);
		final ApiResult<VolunteerGroup> groupResult = this.database.addGroup(group);
		if (groupResult.isError()) {
			this.logger.severe(groupResult.getText());
			return new ApiResult<Location>(groupResult);
		}

		final Map<String, String> options = new TreeMap<String, String>();
		options.put(VinnyOption.PRODUCT_NAME, Engine.DEFAULT_PRODUCT_NAME);
		options.put(VinnyOption.AUTO_AUTHORIZE, "true");
		options.put(VinnyOption.MINIMUM_SERVICE_TIME, "5");
		options.put(VinnyOption.ICON_FILE_NAME, "images/256/trackin_it.png");
		options.put(VinnyOption.SERVICE_OF, Engine.DEFAULT_SERVICE_OF);
		options.put(VinnyOption.DISPLAY_SERVICE_TYPES, "false");
		options.put(VinnyOption.USE_OPERATING_HOURS, "true");
		options.put(VinnyOption.FIRST_MONTH_OF_YEAR, "Jan");
		options.put(VinnyOption.DEFAULT_SERVICE_TIME, "300");

		for (final String optionName : options.keySet()) {
			final VinnyOption option = new VinnyOption(optionName, options.get(optionName), installLocation);
			this.database.setOption(option);
		}

		return new ApiResult<Location>(this.location);
	}

	@Override
	public ApiResult<Workstation> installWorkstation(final String hostName) {
		ValidateUtil.assertNotNull(this.location, "Location must be set before call to add workstation.");
		final Workstation tmp = new Workstation();
		tmp.setActive(true);
		tmp.setName(hostName);
		tmp.setLocation(this.location);
		return this.database.addWorkstation(tmp);
	}

	@Override
	public ApiResult<Card> isCardAvailable(final Volunteer volunteer, final String cardNumber) {
		final ApiResult<Card> cardResult = this.database.getCard(cardNumber);
		if (cardResult.isError()) {
			return new ApiResult<Card>(false, cardNumber + " is available.");
		}
		final Card card = cardResult.getResult();
		if (card.getVolunteer().equals(volunteer)) {
			return cardResult;
		}
		return new ApiResult<Card>(true, cardNumber + " is already assigned to " + card.getVolunteer().getFirstName() + " " + card.getVolunteer().getLastName());
	}

	/*
	 * (non-Javadoc)
	 * @see org.aergia.vinny.engine.Engine#isDatabaseInitialized()
	 */
	@Override
	public boolean isDatabaseInitialized() {
		return this.database.isInitialized();
	}

	@Override
	public boolean isInOperatingHours(final DayOfWeek day, final int hour) {

		final ApiResult<OperatingHours> apiResult = this.database.getOperatingHours(day);
		if (apiResult.isError()) {
			return false;
		}
		final OperatingHours operatingHour = apiResult.getResult();
		final int startHour = operatingHour.getStartHour();
		final int endHour = operatingHour.getEndHour();
		return (startHour <= hour) && (hour <= endHour);
	}

	@Override
	public boolean isUserNameInUse(final String userName) {
		final ApiResult<VinnyUser> result = this.database.getUser(userName);
		return result.getResult() != null;
	}

	@Override
	public boolean isVinnyCard(final String scanned) {
		if ((scanned == null) || (scanned.length() != EngineImpl.VINNY_CARD_NUMBER_LENGTH)) {
			return false;
		}
		try {
			Long.parseLong(scanned);
		} catch (final NumberFormatException nfe) {
			return false;
		}
		return true;
	}

	@Override
	public ApiResult<Volunteer> modifyVolunteer(final Volunteer volunteer, final VinnyUser user) {
		ValidateUtil.assertNotNull(volunteer, "Volunteer must be specified.");
		ValidateUtil.assertNotNull(user, "The user must be specified.");

		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to modify a volunteer.");
		}

		return this.database.saveVolunteer(volunteer);
	}

	@Override
	public ApiResult<VolunteerGroup> removeGroup(final VolunteerGroup group, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<VolunteerGroup>(true, "User " + user.getUserName() + " does not have permission to remove a volunteer group.");
		}
		final ApiResult<VolunteerGroup> defaultGroupResult = this.database.getGroup(VolunteerGroup.GENERAL_GROUP, this.location);
		if (!defaultGroupResult.isError()) {
			final VolunteerGroup defaultGroup = defaultGroupResult.getResult();
			if (group.equals(defaultGroup)) {
				return new ApiResult<VolunteerGroup>(true, "Cannot disable " + VolunteerGroup.GENERAL_GROUP + ".");
			}

			final Collection<Volunteer> volunteers = this.database.getVolunteers(group);
			for (final Volunteer volunteer : volunteers) {
				volunteer.setGroup(defaultGroup);
				this.database.saveVolunteer(volunteer);
			}
		}
		group.setActive(false);
		return this.database.saveGroup(group);
	}

	@Override
	public ApiResult<ServiceType> removeServiceType(final ServiceType serviceType, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<ServiceType>(true, "User " + user.getUserName() + " does not have permission to remove a service type.");
		}
		final ApiResult<ServiceType> defaultServiceTypeResult = this.database.getServiceType(ServiceType.DEFAULT_SERVICE, this.location);
		if (!defaultServiceTypeResult.isError()) {
			final ServiceType defaultServiceType = defaultServiceTypeResult.getResult();
			if (serviceType.equals(defaultServiceType)) {
				return new ApiResult<ServiceType>(true, "Cannot disable " + ServiceType.DEFAULT_SERVICE + ".");
			}
		}
		serviceType.setActive(false);
		return this.database.saveServiceType(serviceType);
	}

	@Override
	public ApiResult<Volunteer> removeSkillFromVolunteer(final Volunteer volunteer, final Skill skill, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to modify a volunteer.");
		}
		if (volunteer.getSkills().contains(skill)) {
			if (volunteer.getSkills().size() == 1) {
				return new ApiResult<Volunteer>(true, "You cannot remove the last skill from this volunteer.");
			}
			volunteer.removeSkill(skill);
			this.database.saveVolunteer(volunteer);
		}
		return new ApiResult<Volunteer>(volunteer);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.aergia.vinny.engine.Engine#deleteUser(org.aergia.vinny.database.pojo
	 * .VinnyUser, org.aergia.vinny.database.pojo.VinnyUser)
	 */
	@Override
	public ApiResult<VinnyUser> removeUser(final VinnyUser deleteUser, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_USER)) {
			return new ApiResult<VinnyUser>(true, "User " + user.getUserName() + " does not have permission to delete a user.");
		}
		if (deleteUser.getUserName().equals(user.getUserName())) {
			return new ApiResult<VinnyUser>(true, "User " + user.getUserName() + " cannot delete user " + deleteUser.getUserName() + ".");
		}
		return this.database.deleteUser(deleteUser);
	}

	/*
	 * (non-Javadoc)
	 * @see
	 * org.aergia.vinny.engine.Engine#removeVolunteer(org.aergia.vinny.database
	 * .pojo.Volunteer, org.aergia.vinny.database.pojo.VinnyUser)
	 */
	@Override
	public ApiResult<Volunteer> removeVolunteer(final Volunteer volunteer, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to remove a volunteer.");
		}
		return this.database.deleteVolunteer(volunteer);
	}

	@Override
	public ApiResult<Workstation> removeWorkstation(final Workstation removeWorkstation, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<Workstation>(true, "User " + user.getUserName() + " does not have permission to remove a workstation.");
		}
		removeWorkstation.setActive(false);
		return this.database.saveWorkstation(removeWorkstation);
	}

	@Override
	public ApiResult<Card> saveCard(final Card card, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Card>(true, "User " + user.getUserName() + " does not have permission to save a card.");
		}
		return this.database.saveCard(card);
	}

	@Override
	public ApiResult<VolunteerGroup> saveGroup(final VolunteerGroup group, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<VolunteerGroup>(true, "User " + user.getUserName() + " does not have permission to save a volunteer group.");
		}
		return this.database.saveGroup(group);
	}

	@Override
	public ApiResult<OperatingHours> saveHours(final OperatingHours hours, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<OperatingHours>(true, "User " + user.getUserName() + " does not have permission to modify operating hours.");
		}
		return this.database.saveOperatingHours(hours);
	}

	@Override
	public ApiResult<VinnyOption> saveOption(final VinnyOption option, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<VinnyOption>(true, "User " + user.getUserName() + " does not have permission to save a options.");
		}
		return this.database.saveOption(option);
	}

	@Override
	public ApiResult<Scan> saveScan(final Scan scan, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_SERVICE)) {
			return new ApiResult<Scan>(true, "User " + user.getUserName() + " does not have permission to save a scan.");
		}
		return this.database.saveScan(scan);
	}

	@Override
	public ApiResult<Service> saveService(final Service service, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_SERVICE)) {
			return new ApiResult<Service>(true, "User " + user.getUserName() + " does not have permission to save a service.");
		}
		return this.database.saveService(service);
	}

	@Override
	public ApiResult<ServiceType> saveServiceType(final ServiceType serviceType, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<ServiceType>(true, "User " + user.getUserName() + " does not have permission to modify a service type.");
		}
		return this.database.saveServiceType(serviceType);
	}

	@Override
	public ApiResult<VinnyUser> saveUser(final VinnyUser saveUser, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_USER)) {
			return new ApiResult<VinnyUser>(true, "User " + user.getUserName() + " does not have permission to modify a user.");
		}
		return this.database.saveUser(saveUser);
	}

	@Override
	public ApiResult<Volunteer> saveVolunteer(final Volunteer volunteer, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_VOLUNTEER)) {
			return new ApiResult<Volunteer>(true, "User " + user.getUserName() + " does not have permission to modify a volunteer.");
		}

		if (volunteer.getId() == 0) {
			return this.database.addVolunteer(volunteer);
		}

		if (!volunteer.isActive() && volunteer.getCard().getEndDate().equals(DateUtil.FAR_IN_FUTURE)) {
			volunteer.getCard().setEndDate(DateUtil.getInstance());
			volunteer.getCard().setComment("In-active volunteer.");
		}

		if (volunteer.isActive() && !volunteer.getCard().getEndDate().equals(DateUtil.FAR_IN_FUTURE)) {
			volunteer.getCard().setEndDate(DateUtil.FAR_IN_FUTURE);
			volunteer.getCard().setComment("Volunteer re-activated.");
		}

		if (volunteer.getSkills().size() == 0) {
			final ApiResult<Skill> skillResult = this.getSkill(Skill.GENERAL_SKILL);
			volunteer.setSkills(Collections.singletonList(skillResult.getResult()));
		}

		return this.database.saveVolunteer(volunteer);
	}

	@Override
	public ApiResult<Workstation> saveWorkstation(final Workstation saveWorkstation, final VinnyUser user) {
		if (!user.inRole(VinnyRole.MODIFY_CONFIGURATION)) {
			return new ApiResult<Workstation>(true, "User " + user.getUserName() + " does not have permission to modify a workstation.");
		}
		if (saveWorkstation.getLocation() == null) {
			saveWorkstation.setLocation(this.location);
		}
		return this.database.saveWorkstation(saveWorkstation);
	}

	@Override
	public ApiResult<Scan> scanCard(final String cardNumber, final Calendar date) {
		ValidateUtil.assertNotNull(cardNumber, "Card number must be specified.");
		ValidateUtil.assertNotNull(date, "Date must be specified.");
		ValidateUtil.assertNotNull(this.workstation, "Worstation has not been set.");

		if (!this.isVinnyCard(cardNumber)) {
			return new ApiResult<Scan>(true, "Invalid scan. Please re-scan your " + this.getProductName() + " card.");
		}
		final ApiResult<Card> cardResult = this.database.getCard(cardNumber);
		if (cardResult.isError()) {
			return new ApiResult<Scan>(true, "Your " + this.getProductName() + " card is not found. Please see the volunteer coordinator.");
		}

		final Card card = cardResult.getResult();
		if (!card.isActive()) {
			return new ApiResult<Scan>(true, "Your " + this.getProductName() + " card is no longer valid. Please see the volunteer coordinator.");
		}

		final Scan scan = new Scan(card, this.workstation, date);
		scan.setLocation(this.location);
		return this.database.addScan(scan);
	}

	@Override
	public void setLocation(final Location location) {
		this.location = location;
		this.validateLocation();
	}

	@Override
	public void setOption(final String name, final String value) {
		final VinnyOption option = new VinnyOption(name, value, this.location);
		this.database.setOption(option);
	}

	@Override
	public void setWorkstation(final Workstation workstation) {
		this.workstation = workstation;
	}

	@Override
	public ApiResult<Card> switchCard(final Card oldCard, final Card newCard) {
		oldCard.setActive(false);
		oldCard.setComment("Replaced by card " + newCard.getNumber());
		oldCard.setEndDate(DateUtil.getInstance());
		newCard.setComment("Replaces card " + oldCard.getNumber());
		this.database.saveCard(oldCard);
		return new ApiResult<Card>(oldCard);
	}

	@Override
	public ApiResult<VinnyUser> validateUser(final String userName, final String password) {
		final ApiResult<VinnyUser> apiResult = this.database.getUser(userName);
		if (apiResult.isError()) {
			if (apiResult.getText().equals("Unknown user")) {
				return new ApiResult<VinnyUser>(true, "User " + userName + " or password is not valid in the system.");
			}
			return apiResult;
		}
		final VinnyUser user = apiResult.getResult();
		if ((user != null) && user.getPassword().equals(Md5Util.encode(password))) {
			return apiResult;
		}
		return new ApiResult<VinnyUser>(true, "User " + userName + " or password is not valid in the system.");
	}

	private VinnyUser getAdminUser() {
		final ApiResult<VinnyUser> result = this.database.getUser("admin");
		if (result.isError()) {
			this.logger.severe("There is no admin user.");
			return null;
		}
		return result.getResult();
	}

	/**
	 * @return default product name
	 */
	private String getProductName() {
		if (this.productName == null) {
			this.productName = "\"" + this.getOption(VinnyOption.PRODUCT_NAME, Engine.DEFAULT_PRODUCT_NAME) + "\"";
		}
		return this.productName;
	}

	private boolean isAutoAuthoriable(final Service service) {
		if (this.isInOperatingHours(service)) {
			return true;
		}
		return false;
	}

	private boolean isInOperatingHours(final Service service) {
		if (DateUtil.isSameDay(service.getStartTime(), service.getEndTime())) {
			final DayOfWeek day = DateUtil.getDayOfWeek(service.getStartTime());
			final ApiResult<OperatingHours> hoursResult = this.database.getOperatingHours(day);
			if (!hoursResult.isError() && (hoursResult.getResult() != null)) {
				final OperatingHours hours = hoursResult.getResult();

				final int startMinutes = DateUtil.getMinutesOfDay(service.getStartTime());
				if (startMinutes < hours.getStartHour()) {
					service.setComment("Start time before opening hour of " + DateUtil.formatHour(hours.getStartHour()));
					return false;
				}

				final int endMinutes = DateUtil.getMinutesOfDay(service.getEndTime());
				if (endMinutes > hours.getEndHour()) {
					service.setComment("Stop time after closing hour of " + DateUtil.formatHour(hours.getEndHour()));
					return false;
				}
				return true;
			}
		}
		return false;
	}

	private boolean validateLocation() {
		if (this.location == null) {
			return false;
		}

		final String serviceOf = this.getOption(VinnyOption.SERVICE_OF, null);
		if (serviceOf == null) {
			this.setOption(VinnyOption.SERVICE_OF, Engine.DEFAULT_SERVICE_OF);
		}

		final String displayServiceType = this.getOption(VinnyOption.DISPLAY_SERVICE_TYPES, null);
		if (displayServiceType == null) {
			this.setOption(VinnyOption.DISPLAY_SERVICE_TYPES, "false");
		}

		final String useOperatingHours = this.getOption(VinnyOption.USE_OPERATING_HOURS, null);
		if (useOperatingHours == null) {
			this.setOption(VinnyOption.USE_OPERATING_HOURS, "true");
		}

		final String firstMonthOfYear = this.getOption(VinnyOption.FIRST_MONTH_OF_YEAR, null);
		if (firstMonthOfYear == null) {
			this.setOption(VinnyOption.FIRST_MONTH_OF_YEAR, "Jan");
		}

		final String defaultServiceTime = this.getOption(VinnyOption.DEFAULT_SERVICE_TIME, null);

		if (defaultServiceTime == null) {
			this.setOption(VinnyOption.DEFAULT_SERVICE_TIME, "300");
		}

		final ApiResult<Workstation> workstationResult = this.database.getWorkstation(Workstation.GENERIC_WORKSTATION, this.location);
		if (workstationResult.isError()) {
			this.addWorkstation(Workstation.GENERIC_WORKSTATION, this.admin);
		}

		final ApiResult<ServiceType> serviceTypeResult = this.database.getServiceType(ServiceType.DEFAULT_SERVICE, this.location);
		if (serviceTypeResult.isError()) {
			this.addServiceType(ServiceType.DEFAULT_SERVICE, this.admin);
		}

		return true;
	}

}
